組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

5.1 ベターCのためのコーディング規約

ベターC(より良いC)としてC++を使うには,少しコツがあります.それは,C++コンパイラが暗黙的なコードを挿入するのを最小限に抑えて,可能なかぎり,実際に行われる処理がソースコード上に現れるようにすることです.そのためには,次の内容をコーディング規約に盛り込むとよいでしょう.

  • 明示的なデストラクタは定義しない.
  • 仮想関数は使用しない.
  • 仮引数を1つ受け取るコンストラクタには,必ずexplicit指定子を付ける.
  • 例外処理は使用しない.

そして,その目的が,C++をベターCとして使用することにあることを明確にすることです.そうでなければ,これらを規定したコーディング規約は,単なる的外れなルールになってしまいます.

では,これらのルールがなぜ必要なのかについて,詳しく見ていくことにしましょう.全体を通じていえることは,これらのルールの目的が,コンパイラによって暗黙的なコードが挿入されるのを防ぐためということです.

現実には,上記の規約を杓子定規に守るのではなく,現場の状況(メンバーの力量や設計方針など)に応じて,適宜取捨選択することが望ましいでしょう.

5.1.1 明示的なデストラクタは定義しない

クラスに明示的なデストラクタを定義すると,オブジェクトが生存期間を終えるとき,デストラクタが暗黙的に呼び出されます.慣れていないと,ソースコードを見ただけではどんな処理が行われるのかが把握しづらいため,ベターCとしてC++を使う場合には,明示的なデストラクタの定義は避けたほうが無難です.

また,明示的なデストラクタを定義したクラスの自動オブジェクトを使用すると,例外が送出されたときにデストラクタを呼び出すためのコードが自動的に挿入されます.この例外処理のためのコードあるいはデータは決して小さいとはいえず,例外処理を活用できるのであればそれは必要なコストですが,活用できなければ単なるオーバーヘッドになってしまいます.

ただし,例外処理をサポートしない処理系や,コンパイルオプションで例外処理を抑止するのであれば,明示的なデストラクタによるオーバーヘッドはそれほど神経質になるほどではありません.そうした状況で,かつC++にある程度慣れた技術者だけで開発を行うのであれば,明示的なデストラクタを定義することを許可したほうが,なにかとメリットが多いかと思います.

5.1.2 仮想関数は使用しない

仮想関数を使用すると,実際にどんな処理が行われるのか,実行時でなければわからなくなります.Cで関数へのポインタを用いた場合も同じですが,関数へのポインタは,呼び出すときに(*func)()のような記述にすることで,それが関数へのポインタであることを表現できたのに対して,仮想関数の場合はそうもいかず,一見して普通のメンバー関数との区別が付きにくくなっています.

また,仮想関数テーブルを用いて仮想関数が実現される場合がほとんどですので,クラスに明示的なコンストラクタを定義しなくても,暗黙的に仮想関数テーブルへのポインタを設定するコードが挿入されますし,使用するかどうかにかかわらず,実行時型識別情報が埋め込まれます.さらに,実際に呼び出される機会があるかどうかにかかわらず,基底クラスのものも含めた全仮想関数がリンクされてしまい,プログラムサイズの肥大化にも繋がります.

どうしても仮想関数を使う必要がある場合には,「2.6.3 NVIイディオム」で紹介したNVIイディオムを使用し,仮想関数には適切なプリフィックスを付けるなどして区別したほうが無難です.

5.1.3 仮引数を1つ受け取るコンストラクタには, 必ずexplicit指定子を付ける

仮引数を1つだけ受け取るコンストラクタ,または2つ以上受け取る場合でも,第2引数以降に省略時実引数が設定されているコンストラクタは,「変換コンストラクタ」(Converting Constructor)と呼ばれ,引数の型からそのクラス型への暗黙的な型変換を可能とするものです.

class A
{
public:
    A(const B& arg);
    A(const C& arg1, int arg2 = 0);
     …
};

上記のコードに示すAクラスには2つのコンストラクタが定義されています.1つ目のコンストラクタは,引数を1つしか受け取らないので,明らかな変換コンストラクタ(「2.2.1 コンストラクタとデストラクタ」参照)です.2つ目のコンストラクタは,引数を2つ受け取りますが,第2引数に省略時実引数が設定されているので,やはり変換コンストラクタになります.これらの変換コンストラクタが定義されていることにより,BまたはC型のオブジェクトからAクラスに暗黙的な型変換が可能になります.この暗黙的な型変換が行われるということは,変換コンストラクタを呼び出し,Aクラスの一時オブジェクトを生成するコードをC++コンパイラが挿入することを意味しています.

変換コンストラクタにexplicit指定子を付けることで,このような暗黙的な型変換は行われなくなり,明示的にコンストラクタを呼び出す(すなわちキャストする)必要が出てきます.不便な状況もあるかもしれませんが,ソースコードを見ただけで何が行われているのか把握しやすくするには,このほうが無難でしょう.

5.1.4 例外処理は使用しない

詳しくは第3章で解説していますが,例外処理を使用しないことで,Cとさほど変わらない感覚で設計およびコーディングすることが可能になります.可能であれば,システム全体から例外処理を完全に排除するほうがより確実ですが,次のポイントを守ることで例外処理を使用せずに済むことでしょう.

  • (A) 送出式(throw)を使用しない.
  • (B) 明示的なデストラクタは定義しない.
  • (C) 位置指定形式以外のデフォルトのnew演算子は使用しない.
  • (D) dynamic_cast演算子を用いた参照型へのキャストは行わない.
  • (E) typeid演算子は使用しない.
  • (F) 自立環境(フリースタンディング環境)用のもの,および標準Cライブラリを除き,標準ライブラリは使用しない.

(A)は当然ですが,(B)は見落としがちなポイントです.明示的なデストラクタがあると,実際に例外が送出されるかどうかにかかわらず,例外が送出されたときにデストラクタを呼び出すためのコードが挿入されます.例外を使用しないのであれば,そのようなコードは単なるオーバーヘッドになってしまいます.(C)~(F)は,処理系によって送出される可能性がある機能を避けるために必要です.(C)に挙げたように,デフォルトのnew演算子の使用は避けるべきですが,多重定義した,例外を送出しないnew演算子であれば使用してもよいでしょう.

  • (A) A* p1 = new A; // ← 例外が送出される可能性あり.
  • (B) A* p2 = new(std::nothrow) A; // ← 例外が送出される可能性なし.
  • (C) char storage[sizeof(A)]; // ← 注意! 境界調整に配慮していない.
  •   A* p3 = new(storage) A; // ← 例外が送出される可能性なし.

クラスAのコンストラクタが例外を送出しないかぎり,上記の(B)と(C)では例外が送出される可能性がありません.(C)の場合は,メモリの割り付けに失敗する可能性さえありません.